@wong2kim/wmux 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Inspired by [cmux](https://github.com/manaflow-ai/cmux) (macOS), wmux brings the
14
14
 
15
15
  ## Install
16
16
 
17
- **Download:** [wmux-1.1.1 Setup.exe](https://github.com/openwong2kim/wmux/releases/latest)
17
+ **Download:** [wmux-1.1.2 Setup.exe](https://github.com/openwong2kim/wmux/releases/latest)
18
18
 
19
19
  Or build from source:
20
20
  ```powershell
@@ -45,13 +45,14 @@ irm https://raw.githubusercontent.com/openwong2kim/wmux/main/install.ps1 | iex
45
45
  - **Vi copy mode** — `Ctrl+Shift+X`
46
46
  - **Search** — `Ctrl+F`
47
47
  - **Unlimited scrollback** — 999,999 lines default
48
+ - **Scrollback persistence** — terminal content saved to disk, restored on restart
48
49
 
49
50
  ### Workspaces
50
51
  - Sidebar with drag-and-drop reordering
51
52
  - `Ctrl+1` ~ `Ctrl+9` quick switch
52
53
  - **Multiview** — `Ctrl+click` workspaces to split-view them simultaneously
53
54
  - `Ctrl+Shift+G` to exit multiview
54
- - Session persistence — everything restored on restart
55
+ - **Session persistence**workspace layout, tabs, cwd, and terminal scrollback all restored on restart
55
56
 
56
57
  ### Browser
57
58
  - Built-in browser panel — `Ctrl+Shift+L`
@@ -171,20 +172,29 @@ The `install.ps1` script auto-installs Python and VS Build Tools if missing.
171
172
 
172
173
  ```
173
174
  Electron Main Process
174
- ├── PTYManager (node-pty)
175
+ ├── PTYManager (node-pty / ConPTY)
175
176
  ├── PTYBridge (data forwarding + ActivityMonitor)
176
177
  ├── AgentDetector (gate-based agent status)
178
+ ├── SessionManager (atomic save with .bak recovery)
179
+ ├── ScrollbackPersistence (dump/load terminal buffers)
177
180
  ├── PipeServer (Named Pipe JSON-RPC)
178
181
  ├── McpRegistrar (auto-registers MCP in ~/.claude.json)
182
+ ├── DaemonClient (optional daemon mode connector)
179
183
  └── ToastManager (OS notifications + taskbar flash)
180
184
 
181
185
  Renderer Process (React 19 + Zustand)
182
186
  ├── PaneContainer (recursive split layout)
183
- ├── Terminal (xterm.js + WebGL)
187
+ ├── Terminal (xterm.js + WebGL + scrollback restore)
184
188
  ├── BrowserPanel (webview + Inspector)
185
189
  ├── NotificationPanel
186
190
  └── Multiview grid
187
191
 
192
+ Daemon Process (optional, standalone)
193
+ ├── DaemonSessionManager (ConPTY lifecycle)
194
+ ├── RingBuffer (circular scrollback buffer)
195
+ ├── StateWriter (session suspend/resume)
196
+ └── DaemonPipeServer (Named Pipe RPC)
197
+
188
198
  MCP Server (stdio)
189
199
  └── Bridges Claude Code ↔ wmux via Named Pipe RPC
190
200
  ```
@@ -4,24 +4,25 @@ exports.handleBrowser = handleBrowser;
4
4
  const client_1 = require("../client");
5
5
  const utils_1 = require("../utils");
6
6
  const BROWSER_HELP = `
7
- wmux browser — Scriptable Browser API
7
+ wmux browser — Browser Commands
8
8
 
9
9
  USAGE
10
10
  wmux browser <subcommand> [args]
11
11
 
12
12
  SUBCOMMANDS
13
- snapshot Return the full page HTML (document.documentElement.outerHTML)
14
- click <selector> Click the first element matching the CSS selector
15
- fill <selector> <text> Set the value of an input matching the CSS selector
16
- eval <code> Execute arbitrary JavaScript in the page context
17
13
  navigate <url> Navigate the active browser surface to a URL
14
+ close Close the browser panel
15
+ session start [--profile <name>] Start a browser session
16
+ session stop Stop the active browser session
17
+ session status Show active session status
18
+ session list List available profiles
18
19
 
19
20
  EXAMPLES
20
- wmux browser snapshot
21
- wmux browser click "#submit-btn"
22
- wmux browser fill "input[name=email]" "user@example.com"
23
- wmux browser eval "document.title"
24
21
  wmux browser navigate "https://example.com"
22
+ wmux browser close
23
+ wmux browser session start --profile login
24
+ wmux browser session status
25
+ wmux browser session list
25
26
  `.trimStart();
26
27
  async function handleBrowser(args, jsonMode) {
27
28
  const sub = args[0];
@@ -32,30 +33,14 @@ async function handleBrowser(args, jsonMode) {
32
33
  }
33
34
  let response;
34
35
  switch (sub) {
35
- // ── browser snapshot ─────────────────────────────────────────────────────
36
- case 'snapshot': {
37
- response = await (0, client_1.sendRequest)('browser.snapshot', {});
38
- if (jsonMode) {
39
- (0, utils_1.printResult)(response);
40
- }
41
- else {
42
- if (!response.ok) {
43
- (0, utils_1.printError)(response);
44
- return;
45
- }
46
- const r = response.result;
47
- process.stdout.write(r?.html ?? '');
48
- }
49
- break;
50
- }
51
- // ── browser click <selector> ─────────────────────────────────────────────
52
- case 'click': {
53
- const selector = rest[0];
54
- if (!selector) {
55
- console.error('Error: browser click requires <selector>');
36
+ // ── browser navigate <url> ───────────────────────────────────────────────
37
+ case 'navigate': {
38
+ const url = rest[0];
39
+ if (!url) {
40
+ console.error('Error: browser navigate requires <url>');
56
41
  process.exit(1);
57
42
  }
58
- response = await (0, client_1.sendRequest)('browser.click', { selector });
43
+ response = await (0, client_1.sendRequest)('browser.navigate', { url });
59
44
  if (jsonMode) {
60
45
  (0, utils_1.printResult)(response);
61
46
  }
@@ -64,19 +49,13 @@ async function handleBrowser(args, jsonMode) {
64
49
  (0, utils_1.printError)(response);
65
50
  return;
66
51
  }
67
- console.log(`Clicked: ${selector}`);
52
+ console.log(`Navigated to: ${url}`);
68
53
  }
69
54
  break;
70
55
  }
71
- // ── browser fill <selector> <text> ───────────────────────────────────────
72
- case 'fill': {
73
- const selector = rest[0];
74
- const text = rest.slice(1).join(' ');
75
- if (!selector) {
76
- console.error('Error: browser fill requires <selector> <text>');
77
- process.exit(1);
78
- }
79
- response = await (0, client_1.sendRequest)('browser.fill', { selector, text });
56
+ // ── browser close ────────────────────────────────────────────────────────
57
+ case 'close': {
58
+ response = await (0, client_1.sendRequest)('browser.close', {});
80
59
  if (jsonMode) {
81
60
  (0, utils_1.printResult)(response);
82
61
  }
@@ -85,48 +64,93 @@ async function handleBrowser(args, jsonMode) {
85
64
  (0, utils_1.printError)(response);
86
65
  return;
87
66
  }
88
- console.log(`Filled "${selector}" with "${text}"`);
67
+ console.log('Browser panel closed.');
89
68
  }
90
69
  break;
91
70
  }
92
- // ── browser eval <code> ──────────────────────────────────────────────────
93
- case 'eval': {
94
- const code = rest.join(' ');
95
- if (!code) {
96
- console.error('Error: browser eval requires <code>');
97
- process.exit(1);
98
- }
99
- response = await (0, client_1.sendRequest)('browser.eval', { code });
100
- if (jsonMode) {
101
- (0, utils_1.printResult)(response);
71
+ // ── browser session <action> ─────────────────────────────────────────────
72
+ case 'session': {
73
+ const action = rest[0];
74
+ if (!action || action === '--help' || action === '-h') {
75
+ console.log('Usage: wmux browser session <start|stop|status|list>');
76
+ process.exit(0);
102
77
  }
103
- else {
104
- if (!response.ok) {
105
- (0, utils_1.printError)(response);
106
- return;
78
+ switch (action) {
79
+ case 'start': {
80
+ const profileIdx = rest.indexOf('--profile');
81
+ const profile = profileIdx !== -1 ? rest[profileIdx + 1] : undefined;
82
+ const params = {};
83
+ if (profile)
84
+ params['profile'] = profile;
85
+ response = await (0, client_1.sendRequest)('browser.session.start', params);
86
+ if (jsonMode) {
87
+ (0, utils_1.printResult)(response);
88
+ }
89
+ else {
90
+ if (!response.ok) {
91
+ (0, utils_1.printError)(response);
92
+ return;
93
+ }
94
+ const r = response.result;
95
+ console.log(`Session started with profile: ${r['profile'] ?? 'default'}`);
96
+ }
97
+ break;
107
98
  }
108
- const r = response.result;
109
- console.log(JSON.stringify(r?.result, null, 2));
110
- }
111
- break;
112
- }
113
- // ── browser navigate <url> ───────────────────────────────────────────────
114
- case 'navigate': {
115
- const url = rest[0];
116
- if (!url) {
117
- console.error('Error: browser navigate requires <url>');
118
- process.exit(1);
119
- }
120
- response = await (0, client_1.sendRequest)('browser.navigate', { url });
121
- if (jsonMode) {
122
- (0, utils_1.printResult)(response);
123
- }
124
- else {
125
- if (!response.ok) {
126
- (0, utils_1.printError)(response);
127
- return;
99
+ case 'stop': {
100
+ response = await (0, client_1.sendRequest)('browser.session.stop', {});
101
+ if (jsonMode) {
102
+ (0, utils_1.printResult)(response);
103
+ }
104
+ else {
105
+ if (!response.ok) {
106
+ (0, utils_1.printError)(response);
107
+ return;
108
+ }
109
+ console.log('Session stopped.');
110
+ }
111
+ break;
128
112
  }
129
- console.log(`Navigated to: ${url}`);
113
+ case 'status': {
114
+ response = await (0, client_1.sendRequest)('browser.session.status', {});
115
+ if (jsonMode) {
116
+ (0, utils_1.printResult)(response);
117
+ }
118
+ else {
119
+ if (!response.ok) {
120
+ (0, utils_1.printError)(response);
121
+ return;
122
+ }
123
+ const r = response.result;
124
+ console.log(`Active profile: ${r['profile'] ?? 'none'}`);
125
+ console.log(`CDP port: ${r['port'] ?? 'none'}`);
126
+ }
127
+ break;
128
+ }
129
+ case 'list': {
130
+ response = await (0, client_1.sendRequest)('browser.session.list', {});
131
+ if (jsonMode) {
132
+ (0, utils_1.printResult)(response);
133
+ }
134
+ else {
135
+ if (!response.ok) {
136
+ (0, utils_1.printError)(response);
137
+ return;
138
+ }
139
+ const profiles = response.result['profiles'];
140
+ if (!profiles || profiles.length === 0) {
141
+ console.log('No profiles found.');
142
+ }
143
+ else {
144
+ for (const p of profiles) {
145
+ console.log(` ${p['name']} (partition: ${p['partition']}, persistent: ${p['persistent']})`);
146
+ }
147
+ }
148
+ }
149
+ break;
150
+ }
151
+ default:
152
+ console.error(`Unknown session action: "${action}". Use start, stop, status, or list.`);
153
+ process.exit(1);
130
154
  }
131
155
  break;
132
156
  }
@@ -51,11 +51,12 @@ SYSTEM COMMANDS
51
51
  capabilities List all supported RPC methods
52
52
 
53
53
  BROWSER COMMANDS
54
- browser snapshot Return the full page HTML of the active browser surface
55
- browser click <selector> Click an element by CSS selector
56
- browser fill <selector> <text> Fill an input field by CSS selector
57
- browser eval <code> Execute JavaScript in the browser context
58
54
  browser navigate <url> Navigate the browser surface to a URL
55
+ browser close Close the browser panel
56
+ browser session start [--profile <name>] Start a browser session
57
+ browser session stop Stop the active browser session
58
+ browser session status Show active session status
59
+ browser session list List available profiles
59
60
 
60
61
  GLOBAL FLAGS
61
62
  --json Output raw JSON (useful for scripting)
@@ -67,9 +68,8 @@ EXAMPLES
67
68
  wmux send "echo hello"
68
69
  wmux notify --title "Done" --body "Build finished"
69
70
  wmux identify --json
70
- wmux browser snapshot
71
71
  wmux browser navigate "https://example.com"
72
- wmux browser click "#login-btn"
72
+ wmux browser close
73
73
  `.trimStart();
74
74
  const WORKSPACE_CMDS = new Set([
75
75
  'list-workspaces',
@@ -45,6 +45,9 @@ exports.IPC = {
45
45
  FS_WATCH: 'fs:watch',
46
46
  FS_UNWATCH: 'fs:unwatch',
47
47
  FS_CHANGED: 'fs:changed',
48
+ // Scrollback persistence
49
+ SCROLLBACK_DUMP: 'scrollback:dump',
50
+ SCROLLBACK_LOAD: 'scrollback:load',
48
51
  };
49
52
  // Named Pipe / Unix socket path for wmux API
50
53
  // Fixed name so MCP clients (e.g. Claude Code) can reconnect across wmux restarts
@@ -25,9 +25,20 @@ exports.ALL_RPC_METHODS = [
25
25
  'system.identify',
26
26
  'system.capabilities',
27
27
  'browser.open',
28
- 'browser.snapshot',
29
- 'browser.click',
30
- 'browser.fill',
31
- 'browser.eval',
32
28
  'browser.navigate',
29
+ 'browser.close',
30
+ 'browser.session.start',
31
+ 'browser.session.stop',
32
+ 'browser.session.status',
33
+ 'browser.session.list',
34
+ 'browser.type.humanlike',
35
+ 'browser.cdp.target',
36
+ 'browser.cdp.info',
37
+ 'daemon.createSession',
38
+ 'daemon.destroySession',
39
+ 'daemon.attachSession',
40
+ 'daemon.detachSession',
41
+ 'daemon.resizeSession',
42
+ 'daemon.listSessions',
43
+ 'daemon.ping',
33
44
  ];
@@ -5,6 +5,15 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
5
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
6
  const zod_1 = require("zod");
7
7
  const wmux_client_1 = require("./wmux-client");
8
+ const PlaywrightEngine_1 = require("./playwright/PlaywrightEngine");
9
+ const navigation_1 = require("./playwright/tools/navigation");
10
+ const interaction_1 = require("./playwright/tools/interaction");
11
+ const inspection_1 = require("./playwright/tools/inspection");
12
+ const state_1 = require("./playwright/tools/state");
13
+ const wait_1 = require("./playwright/tools/wait");
14
+ const file_1 = require("./playwright/tools/file");
15
+ const utility_1 = require("./playwright/tools/utility");
16
+ const extraction_1 = require("./playwright/tools/extraction");
8
17
  const server = new mcp_js_1.McpServer({
9
18
  name: 'wmux',
10
19
  version: '1.0.0',
@@ -15,30 +24,29 @@ async function callRpc(method, params = {}) {
15
24
  const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
16
25
  return { content: [{ type: 'text', text }] };
17
26
  }
18
- // Optional surfaceId schema used by browser and terminal tools
19
- const optionalSurfaceId = zod_1.z.string().optional().describe('Target a specific surface by ID. Omit to use the active surface.');
20
- // === Browser tools ===
27
+ // === Browser tools (RPC-based: surface management stays in main process) ===
21
28
  server.tool('browser_open', 'Open a new browser panel in the active pane. Use this when no browser surface exists yet.', {
22
29
  url: zod_1.z.string().optional().describe('Initial URL to load (defaults to google.com)'),
23
30
  }, async ({ url }) => callRpc('browser.open', url ? { url } : {}));
24
- server.tool('browser_navigate', 'Navigate the wmux browser panel to a URL', {
25
- url: zod_1.z.string().describe('The URL to navigate to'),
26
- surfaceId: optionalSurfaceId,
27
- }, async ({ url, surfaceId }) => callRpc('browser.navigate', { url, ...(surfaceId && { surfaceId }) }));
28
- server.tool('browser_snapshot', 'Get the full HTML content of the current page in the wmux browser panel', { surfaceId: optionalSurfaceId }, async ({ surfaceId }) => callRpc('browser.snapshot', surfaceId ? { surfaceId } : {}));
29
- server.tool('browser_click', 'Click an element in the wmux browser panel by CSS selector', {
30
- selector: zod_1.z.string().describe('CSS selector of the element to click'),
31
- surfaceId: optionalSurfaceId,
32
- }, async ({ selector, surfaceId }) => callRpc('browser.click', { selector, ...(surfaceId && { surfaceId }) }));
33
- server.tool('browser_fill', 'Fill an input field in the wmux browser panel by CSS selector', {
34
- selector: zod_1.z.string().describe('CSS selector of the input element'),
35
- text: zod_1.z.string().describe('Text to fill into the input'),
36
- surfaceId: optionalSurfaceId,
37
- }, async ({ selector, text, surfaceId }) => callRpc('browser.fill', { selector, text, ...(surfaceId && { surfaceId }) }));
38
- server.tool('browser_eval', 'Execute JavaScript in the wmux browser panel and return the result', {
39
- code: zod_1.z.string().describe('JavaScript code to execute in the browser context'),
40
- surfaceId: optionalSurfaceId,
41
- }, async ({ code, surfaceId }) => callRpc('browser.eval', { code, ...(surfaceId && { surfaceId }) }));
31
+ server.tool('browser_close', 'Close the browser panel in the active pane', {
32
+ surfaceId: zod_1.z.string().optional().describe('Target a specific surface by ID. Omit to use the active surface.'),
33
+ }, async ({ surfaceId }) => callRpc('browser.close', surfaceId ? { surfaceId } : {}));
34
+ // === Playwright browser tools ===
35
+ (0, navigation_1.registerNavigationTools)(server);
36
+ (0, interaction_1.registerInteractionTools)(server);
37
+ (0, inspection_1.registerInspectionTools)(server);
38
+ (0, state_1.registerStateTools)(server);
39
+ (0, wait_1.registerWaitTools)(server);
40
+ (0, file_1.registerFileTools)(server);
41
+ (0, utility_1.registerUtilityTools)(server);
42
+ (0, extraction_1.registerExtractionTools)(server);
43
+ // === Browser session tools ===
44
+ server.tool('browser_session_start', 'Start a browser session with the specified profile', {
45
+ profile: zod_1.z.string().optional().describe('Profile name to use (defaults to "default")'),
46
+ }, async ({ profile }) => callRpc('browser.session.start', profile ? { profile } : {}));
47
+ server.tool('browser_session_stop', 'Stop the current browser session', {}, async () => callRpc('browser.session.stop'));
48
+ server.tool('browser_session_status', 'Get current browser session status', {}, async () => callRpc('browser.session.status'));
49
+ server.tool('browser_session_list', 'List available browser profiles', {}, async () => callRpc('browser.session.list'));
42
50
  // === Terminal tools ===
43
51
  server.tool('terminal_read', 'Read the current visible text from the active terminal in wmux', {}, async () => callRpc('input.readScreen'));
44
52
  server.tool('terminal_send', 'Send text to the active terminal in wmux', { text: zod_1.z.string().describe('Text to send to the terminal') }, async ({ text }) => callRpc('input.send', { text }));
@@ -53,6 +61,18 @@ server.tool('pane_list', 'List all panes in the current workspace', {}, async ()
53
61
  async function main() {
54
62
  const transport = new stdio_js_1.StdioServerTransport();
55
63
  await server.connect(transport);
64
+ // Clean up Playwright connection when transport closes
65
+ transport.onclose = async () => {
66
+ console.log('[wmux-mcp] Transport closed, disconnecting Playwright');
67
+ await PlaywrightEngine_1.PlaywrightEngine.getInstance().disconnect();
68
+ };
69
+ // Graceful shutdown
70
+ const shutdown = async () => {
71
+ await PlaywrightEngine_1.PlaywrightEngine.getInstance().disconnect();
72
+ process.exit(0);
73
+ };
74
+ process.on('SIGTERM', shutdown);
75
+ process.on('SIGINT', shutdown);
56
76
  }
57
77
  main().catch((err) => {
58
78
  console.error('wmux MCP server failed to start:', err);
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PlaywrightEngine = void 0;
4
+ const playwright_core_1 = require("playwright-core");
5
+ const wmux_client_1 = require("../wmux-client");
6
+ const MAX_CONNECT_RETRIES = 3;
7
+ const RETRY_DELAY_MS = 1000;
8
+ const PAGE_FIND_RETRIES = 5;
9
+ const PAGE_FIND_DELAY_MS = 800;
10
+ function sleep(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+ /**
14
+ * Returns true if the URL belongs to the Electron main renderer window.
15
+ * Navigating these pages would destroy the app — they must never be returned.
16
+ */
17
+ function isElectronShellUrl(url) {
18
+ return (url.startsWith('http://localhost:') ||
19
+ url.startsWith('devtools://') ||
20
+ url.startsWith('chrome://'));
21
+ }
22
+ /**
23
+ * PlaywrightEngine -- singleton wrapper around playwright-core's Chromium CDP connection.
24
+ *
25
+ * Connects to the wmux Electron app via Chrome DevTools Protocol and provides
26
+ * access to browser pages for automation.
27
+ */
28
+ class PlaywrightEngine {
29
+ constructor() {
30
+ this.browser = null;
31
+ this.cdpPort = null;
32
+ }
33
+ static getInstance() {
34
+ if (!PlaywrightEngine.instance) {
35
+ PlaywrightEngine.instance = new PlaywrightEngine();
36
+ }
37
+ return PlaywrightEngine.instance;
38
+ }
39
+ async connect(cdpPort) {
40
+ if (this.browser && this.cdpPort === cdpPort && this.browser.isConnected()) {
41
+ return;
42
+ }
43
+ await this.disconnect();
44
+ this.browser = await playwright_core_1.chromium.connectOverCDP(`http://localhost:${cdpPort}`);
45
+ this.cdpPort = cdpPort;
46
+ console.log(`[PlaywrightEngine] Connected to CDP on port ${cdpPort}`);
47
+ }
48
+ async disconnect() {
49
+ if (this.browser) {
50
+ this.browser = null;
51
+ this.cdpPort = null;
52
+ console.log('[PlaywrightEngine] Disconnected');
53
+ }
54
+ }
55
+ /**
56
+ * Force reconnect — drops existing connection and creates a fresh one.
57
+ * Needed when new webviews are created after the initial connection,
58
+ * because connectOverCDP only discovers targets at connection time.
59
+ */
60
+ async reconnect() {
61
+ const info = (await (0, wmux_client_1.sendRpc)('browser.cdp.info'));
62
+ await this.disconnect();
63
+ await this.connect(info.cdpPort);
64
+ }
65
+ async ensureConnected() {
66
+ if (this.browser?.isConnected())
67
+ return;
68
+ for (let attempt = 1; attempt <= MAX_CONNECT_RETRIES; attempt++) {
69
+ try {
70
+ const info = (await (0, wmux_client_1.sendRpc)('browser.cdp.info'));
71
+ await this.connect(info.cdpPort);
72
+ return;
73
+ }
74
+ catch (err) {
75
+ console.warn(`[PlaywrightEngine] Connection attempt ${attempt}/${MAX_CONNECT_RETRIES} failed:`, err instanceof Error ? err.message : String(err));
76
+ if (attempt < MAX_CONNECT_RETRIES) {
77
+ await sleep(RETRY_DELAY_MS);
78
+ }
79
+ }
80
+ }
81
+ throw new Error(`[PlaywrightEngine] Failed to connect after ${MAX_CONNECT_RETRIES} attempts`);
82
+ }
83
+ /**
84
+ * Collect all Playwright Page objects from all contexts.
85
+ */
86
+ getAllPages() {
87
+ if (!this.browser || !this.browser.isConnected())
88
+ return [];
89
+ const pages = [];
90
+ for (const ctx of this.browser.contexts()) {
91
+ pages.push(...ctx.pages());
92
+ }
93
+ return pages;
94
+ }
95
+ /**
96
+ * Fetch the CDP /json target list.
97
+ */
98
+ async fetchJsonTargets() {
99
+ if (!this.cdpPort)
100
+ return [];
101
+ const resp = await fetch(`http://127.0.0.1:${this.cdpPort}/json`);
102
+ return (await resp.json());
103
+ }
104
+ /**
105
+ * Try to find a Playwright Page that corresponds to a registered webview target.
106
+ * Returns null if no safe page can be found.
107
+ */
108
+ async findWebviewPage(allPages, target) {
109
+ // Strategy 1: Match by targetId → URL from /json endpoint
110
+ if (target) {
111
+ try {
112
+ const jsonTargets = await this.fetchJsonTargets();
113
+ const jsonTarget = jsonTargets.find((t) => t.id === target.targetId);
114
+ if (jsonTarget && !isElectronShellUrl(jsonTarget.url)) {
115
+ // Find Playwright page with matching URL
116
+ const matched = allPages.find((p) => p.url() === jsonTarget.url);
117
+ if (matched)
118
+ return matched;
119
+ // URL might differ slightly (trailing slash, redirect) — try loose match
120
+ const normalizedTarget = jsonTarget.url.replace(/\/+$/, '');
121
+ const looseMatch = allPages.find((p) => p.url().replace(/\/+$/, '') === normalizedTarget);
122
+ if (looseMatch)
123
+ return looseMatch;
124
+ }
125
+ }
126
+ catch {
127
+ // /json fetch failed
128
+ }
129
+ }
130
+ // Strategy 2: Any page that isn't the Electron shell
131
+ // about:blank is allowed — webviews start there before navigating
132
+ const candidates = allPages.filter((p) => !isElectronShellUrl(p.url()));
133
+ if (candidates.length > 0) {
134
+ return candidates[0];
135
+ }
136
+ return null;
137
+ }
138
+ /**
139
+ * Get a Page matching the given surfaceId.
140
+ *
141
+ * Includes retry logic: if no webview page is found on the first attempt,
142
+ * reconnects to CDP (to discover newly created webview targets) and retries.
143
+ */
144
+ async getPage(surfaceId) {
145
+ await this.ensureConnected();
146
+ for (let attempt = 1; attempt <= PAGE_FIND_RETRIES; attempt++) {
147
+ const allPages = this.getAllPages();
148
+ if (allPages.length === 0 && attempt < PAGE_FIND_RETRIES) {
149
+ // No pages yet — reconnect to discover new targets
150
+ await sleep(PAGE_FIND_DELAY_MS);
151
+ await this.reconnect();
152
+ continue;
153
+ }
154
+ // Get registered webview targets
155
+ const info = (await (0, wmux_client_1.sendRpc)('browser.cdp.info'));
156
+ const target = surfaceId
157
+ ? info.targets.find((t) => t.surfaceId === surfaceId)
158
+ : info.targets[0];
159
+ // If no targets registered yet, wait for webview to initialize
160
+ if (!target && attempt < PAGE_FIND_RETRIES) {
161
+ console.log(`[PlaywrightEngine] No CDP targets registered yet, retry ${attempt}/${PAGE_FIND_RETRIES}...`);
162
+ await sleep(PAGE_FIND_DELAY_MS);
163
+ // Reconnect to pick up newly created webview targets
164
+ await this.reconnect();
165
+ continue;
166
+ }
167
+ const page = await this.findWebviewPage(allPages, target);
168
+ if (page)
169
+ return page;
170
+ // Page not found — reconnect and retry (new webview might not be visible yet)
171
+ if (attempt < PAGE_FIND_RETRIES) {
172
+ console.log(`[PlaywrightEngine] Webview page not found, reconnecting... (${attempt}/${PAGE_FIND_RETRIES})`);
173
+ await sleep(PAGE_FIND_DELAY_MS);
174
+ await this.reconnect();
175
+ }
176
+ }
177
+ console.warn('[PlaywrightEngine] No webview page found after all retries');
178
+ return null;
179
+ }
180
+ async getBrowser() {
181
+ await this.ensureConnected();
182
+ return this.browser;
183
+ }
184
+ }
185
+ exports.PlaywrightEngine = PlaywrightEngine;
186
+ PlaywrightEngine.instance = null;